﻿using System;
using System.Collections.Generic;
using System.IO;
using log4net;
using Mono.Cecil;
using Mono.Cecil.Binary;
using Mono.Cecil.Cil;

namespace CILADA.Engine
{
    public class AssemblyReader
    {
        #region Fields
        private static readonly ILog log = LogManager.GetLogger(typeof(AssemblyReader));
        #endregion

        #region Properties
        public string Container { get; set; }
        #endregion

        #region Instance Methods
        public AssemblyDefinition LoadAssembly(string filePath)
        {
            log.Info("Loading assembly " + filePath);
            FileInfo assemblyFile = new FileInfo(filePath);

            if (File.Exists(assemblyFile.FullName))
            {
                if (AssemblyUtils.IsDotNetAssembly(assemblyFile.FullName))
                {
                    AssemblyDefinition _loadedAssembly = AssemblyFactory.GetAssembly(assemblyFile.FullName);
                    return _loadedAssembly;
                }
                else
                {
                    BadImageFormatException bife = new BadImageFormatException("Assembly '" + assemblyFile.Name + "' is not a .NET assembly.");
                    log.Warn("Failed to load assembly " + assemblyFile.FullName, bife);
                    throw bife;
                }
            }
            else
            {
                FileNotFoundException fnfe = new FileNotFoundException("Assembly '" + assemblyFile.Name + "' does not exist.");
                log.Warn("Failed to load assembly " + assemblyFile.FullName, fnfe);
                throw fnfe;
            }
        }

        public ICollection<TypeInfo> GetDepedenciesForAssembly(AssemblyDefinition assembly, ref IDictionary<string, TypeInfo> typeInfoCache)
        {
            log.Info("Getting dependencies for " + assembly.Name.Name);

            foreach (ModuleDefinition module in assembly.Modules)
            {
                foreach (TypeDefinition type in module.ExternTypes)
                {
                    log.Debug("Getting dependecies for external type " + type.FullName);
                    AddTypeToCache(type, ref typeInfoCache);
                    foreach (Dependency dep in AnalyzeType(type))
                    {
                        ProcessDependency(dep, ref typeInfoCache);
                    }
                }

                foreach (TypeDefinition type in module.Types)
                {
                    if ("<Module>".Equals(type.Name))
                    {
                        continue;
                    }

                    log.Debug("Getting dependecies for type " + type.FullName);
                    AddTypeToCache(type, ref typeInfoCache);
                    foreach (Dependency dep in AnalyzeType(type))
                    {
                        ProcessDependency(dep, ref typeInfoCache);
                    }
                }
            }

            // sort the results so they are easier to read
            foreach (TypeInfo ti in typeInfoCache.Values)
            {
                ti.DependsOn.Sort();
            }

            return typeInfoCache.Values;
        }

        private void AddTypeToCache(TypeDefinition type, ref IDictionary<string, TypeInfo> typeInfoCache)
        {
            string typeName = TypeUtilities.FormatItemIntoType(type.FullName);
            TypeInfo ti = new TypeInfo(typeName, this.Container, type);

            if (!typeInfoCache.ContainsKey(typeName))
            {
                typeInfoCache.Add(typeName, ti);
            }
            else if (typeInfoCache.ContainsKey(typeName) && !typeInfoCache[typeName].CreatedWithType)
            {
                typeInfoCache[typeName].UpdateWithTypeInfo(ti);
            }
        }

        private void ProcessDependency(Dependency dep, ref IDictionary<string, TypeInfo> typeInfoCache)
        {
            log.Debug("Processing dependency: " + dep.ToString());
            // get the TypeInfo of the using type
            TypeInfo usingTypeInfo;
            if (typeInfoCache.ContainsKey(dep.UsingType))
            {
                usingTypeInfo = typeInfoCache[dep.UsingType];
            }
            else
            {
                usingTypeInfo = new TypeInfo(dep.UsingType, this.Container);
                typeInfoCache.Add(dep.UsingType, usingTypeInfo);
            }

            // get the TypeInfo of the used type
            TypeInfo usedTypeInfo;
            if (typeInfoCache.ContainsKey(dep.UsedType))
            {
                usedTypeInfo = typeInfoCache[dep.UsedType];
            }
            else
            {
                usedTypeInfo = new TypeInfo(dep.UsedType);
                typeInfoCache.Add(dep.UsedType, usedTypeInfo);
            }

            // create the new dependency and add it to the using types list of dependencies
            TypeDependency typeDep = new TypeDependency(usedTypeInfo, dep.RelationshipClassification);
            if (!usingTypeInfo.DependsOn.Contains(typeDep))
            {
                usingTypeInfo.AddDependency(typeDep);
            }
        }
        #region DNC Code
        /**
         * The classes and methods defined from here on in this file are mostly taken from the dotnetarchitect project, available at http://sourceforge.net/projects/dotnetarchitect/
         * These classes are all from the DependencyChecker namespace, http://dotnetarchitect.svn.sourceforge.net/viewvc/dotnetarchitect/src/DependencyChecker/
         * They were released under the LGPL license.
         * 
         * There are some differences between these and the originals.
         **/

        public IEnumerable<Dependency> AnalyzeType(TypeDefinition type)
        {
            FullNameToken callingToken = GetFullnameToken(type);

            if (type.BaseType != null && !IsLinked(type.BaseType, type.DeclaringType))
            {
                foreach (Dependency dependency in GetDependencies(callingToken, type.BaseType, null, null, RelationshipClassification.extends))
                {
                    yield return dependency;
                }
            }

            foreach (TypeReference interfaceRef in type.Interfaces)
            {
                foreach (Dependency dependency in GetDependencies(callingToken, interfaceRef, null, null, RelationshipClassification.implements))
                {
                    yield return dependency;
                }
            }

            foreach (FieldDefinition field in type.Fields)
            {
                if (IsLinked(field.FieldType, type.DeclaringType))
                {
                    foreach (Dependency dependency in GetDependencies(callingToken, field.FieldType, null, null))
                    {
                        yield return dependency;
                    }
                }
            }

            foreach (EventDefinition @event in type.Events)
            {
                foreach (Dependency dependency in GetDependencies(callingToken, @event.EventType, null, null))
                {
                    yield return dependency;
                }
            }

            foreach (PropertyDefinition property in type.Properties)
            {
                if (!IsLinked(property.PropertyType, type.DeclaringType))
                {
                    foreach (Dependency dependency in GetDependencies(callingToken, property.PropertyType, null, null))
                    {
                        yield return dependency;
                    }
                }
            }

            foreach (CustomAttribute customAttribute in type.CustomAttributes)
            {
                foreach (
                    Dependency dependency in
                        GetDependencies(callingToken, customAttribute.Constructor.DeclaringType, null, null))
                {
                    yield return dependency;
                }
            }

            foreach (MethodDefinition method in type.Constructors)
            {
                callingToken = GetFullnameToken(type, method.Name);
                foreach (Dependency dependency in AnalyzeMethod(type, callingToken, method))
                {
                    yield return dependency;
                }
            }

            foreach (MethodDefinition method in type.Methods)
            {
                callingToken = GetFullnameToken(type, method.Name);
                foreach (Dependency dependency in AnalyzeMethod(type, callingToken, method))
                {
                    yield return dependency;
                }
            }

            foreach (TypeDefinition nestedType in type.NestedTypes)
            {
                foreach (Dependency dependency in AnalyzeType(nestedType))
                {
                    yield return dependency;
                }
            }
        }

        private IEnumerable<Dependency> AnalyzeMethod(TypeDefinition owner, FullNameToken callingToken,
                                                             MethodDefinition method)
        {
            foreach (CustomAttribute customAttribute in method.CustomAttributes)
            {
                foreach (
                    Dependency dependency in
                        GetDependencies(callingToken, customAttribute.Constructor.DeclaringType, null, null))
                {
                    yield return dependency;
                }
                foreach (
                    Dependency dependency in
                        GetDependencies(callingToken, customAttribute.Constructor.DeclaringType,
                                        customAttribute.Constructor.Name, null))
                {
                    yield return dependency;
                }
            }

            foreach (ParameterDefinition parameter in method.Parameters)
            {
                foreach (Dependency dependency in GetDependencies(callingToken, parameter.ParameterType, null, null))
                {
                    yield return dependency;
                }
            }

            if (method.ReturnType != null && !IsLinked(method.ReturnType.ReturnType, method.DeclaringType.DeclaringType))
            {
                foreach (
                    Dependency dependency in GetDependencies(callingToken, method.ReturnType.ReturnType, null, null))
                {
                    yield return dependency;
                }
            }

            if (method.HasBody)
            {
                foreach (VariableDefinition variable in method.Body.Variables)
                {
                    if (!IsLinked(variable.VariableType, method.DeclaringType.DeclaringType))
                    {
                        foreach (
                            Dependency dependency in GetDependencies(callingToken, variable.VariableType, null, null))
                        {
                            yield return dependency;
                        }
                    }
                }

                SequencePoint mostRecentSeqPoint = null;
                foreach (Instruction instruction in method.Body.Instructions)
                {
                    if (instruction.SequencePoint != null)
                    {
                        mostRecentSeqPoint = instruction.SequencePoint;
                    }
                    foreach (
                        Dependency dependency in
                            AnalyzeInstruction(owner, callingToken, instruction, mostRecentSeqPoint))
                    {
                        yield return dependency;
                    }
                }
            }
        }

        private IEnumerable<Dependency> AnalyzeInstruction(TypeDefinition owner, FullNameToken callingToken,
                                                                  Instruction instruction, SequencePoint sequencePoint)
        {
            var methodReference = instruction.Operand as MethodReference;

            if (methodReference != null && !IsLinked(methodReference.DeclaringType, owner))
            {
                foreach (
                    Dependency dependency in
                        GetDependencies(callingToken, methodReference.DeclaringType, methodReference.Name, sequencePoint)
                    )
                {
                    yield return dependency;
                }
                foreach (
                    Dependency dependency in
                        GetDependencies(callingToken, methodReference.ReturnType.ReturnType, null, sequencePoint))
                {
                    yield return dependency;
                }
            }

            var field = instruction.Operand as FieldDefinition;
            if (field != null && !IsLinked(field.DeclaringType, owner))
            {
                foreach (Dependency dependency in GetDependencies(callingToken, field.FieldType, null, sequencePoint))
                {
                    yield return dependency;
                }
                foreach (
                    Dependency dependency in
                        GetDependencies(callingToken, field.DeclaringType, field.Name, sequencePoint))
                {
                    yield return dependency;
                }
            }

            var property = instruction.Operand as PropertyDefinition;
            if (property != null && !IsLinked(property.DeclaringType, owner))
            {
                foreach (
                    Dependency dependency in GetDependencies(callingToken, property.PropertyType, null, sequencePoint))
                {
                    yield return dependency;
                }
                foreach (
                    Dependency dependency in
                        GetDependencies(callingToken, property.DeclaringType, property.Name, sequencePoint))
                {
                    yield return dependency;
                }
            }
        }

        private bool IsLinked(TypeReference referringType, TypeReference referrer)
        {
            if (referrer == null || referringType == null)
            {
                return false;
            }
            if (referringType == referrer)
            {
                return true;
            }
            return IsLinked(referringType, referrer.DeclaringType) || IsLinked(referringType.DeclaringType, referrer);
        }

        private TypeInformation GetTypeInfo(TypeReference reference)
        {
            var ti = new TypeInformation();
            if (reference.DeclaringType != null)
            {
                TypeInformation parent = GetTypeInfo(reference.DeclaringType);
                ti.NamespaceName = parent.NamespaceName;
                ti.ClassName = parent.ClassName;
                ti.NestedClassName = parent.NestedClassName + "/" + CleanClassName(reference.Name);
            }
            else
            {
                ti.NamespaceName = reference.Namespace;
                ti.ClassName = CleanClassName(reference.Name);
                ti.NestedClassName = null;
            }

            return ti;
        }

        private FullNameToken GetFullnameToken(TypeReference typeReference)
        {
            return GetFullnameToken(typeReference, null);
        }

        private FullNameToken GetFullnameToken(TypeReference typeReference, string methodName)
        {
            TypeInformation ti = GetTypeInfo(typeReference);
            string namespaceName = ti.NamespaceName;
            string className = ti.ClassName;
            string nestedName = ti.NestedClassName;

            if (!String.IsNullOrEmpty(namespaceName))
            {
                namespaceName = namespaceName + ".";
            }

            if (!String.IsNullOrEmpty(methodName))
            {
                methodName = "::" + methodName;
            }

            return new FullNameToken(namespaceName, className, nestedName, methodName);
        }

        private string CleanClassName(string className)
        {
            if (!String.IsNullOrEmpty(className))
            {
                while (className.EndsWith("[]"))
                {
                    className = className.Substring(0, className.Length - 2);
                }
                int pos = className.LastIndexOf('`');
                if (pos > 0)
                {
                    className = className.Substring(0, pos);
                }
            }
            return className;
        }

        private IEnumerable<Dependency> GetDependencies(FullNameToken callingToken, TypeReference calledType,
                                                               string methodName,
                                                               SequencePoint sequencePoint)
        {
            return GetDependencies(callingToken, calledType, methodName, sequencePoint, RelationshipClassification.uses);
        }

        private IEnumerable<Dependency> GetDependencies(FullNameToken callingToken, TypeReference calledType,
                                                               string methodName,
                                                               SequencePoint sequencePoint, RelationshipClassification relationshipClassification)
        {
            // TODO: Review the rational of making a copy of the calledType to use to get generic params.
            // Seems to work though. Is there much of a speed impact?
            // Add this here so that the generic instanceType can work, even if the calledType is a TypeSpecification.
            TypeReference calledTypeOrig = calledType;

            if (calledType is TypeSpecification)
            {
                // E.g. the reference type System.Int32&, which is used for out parameters.
                // or an arraytype?!?
                calledType = ((TypeSpecification)calledType).ElementType;
            }

            if (!(calledType is GenericInstanceType) && !(calledType is GenericParameter))
            {
                // Currently, we do not look at generic type parameters; we would have to
                // untangle the usage of an actual (non-type-parameter) type's member
                // to get a useful dependency for the user.
                FullNameToken calledToken = GetFullnameToken(calledType, methodName);
                string fileName = null;
                uint startLine = 0;
                if (sequencePoint != null)
                {
                    fileName = sequencePoint.Document.Url;
                    startLine = (uint)sequencePoint.StartLine;
                }
                yield return new Dependency(callingToken, calledToken, fileName, startLine, 0, 0, 0, relationshipClassification);
            }

            var genericInstanceType = calledTypeOrig as GenericInstanceType;
            if (genericInstanceType != null)
            {
                foreach (TypeReference genericArgument in genericInstanceType.GenericArguments)
                {
                    foreach (
                        Dependency dependency in GetDependencies(callingToken, genericArgument, null, sequencePoint))
                    {
                        yield return dependency;
                    }
                }
            }
        }
        #endregion
        #endregion

        #region Nested Types

        private class TypeInformation
        {
            public string ClassName;
            public string NamespaceName;
            public string NestedClassName;
        }

        #endregion
    }
}